import axios, { AxiosInstance, AxiosResponse } from 'axios'
import { configs } from '../config'
import { storage } from './storage'
import utils from './utils'

type APIQueryArgCollection = ApiTypes.APIBodyArgCollection
type APIBodyArgCollection = ApiTypes.APIBodyArgCollection
type ApiBaseResult = ApiTypes.ApiBaseResult
type HTTPStatusCode = ApiTypes.HTTPStatusCode

const BASE_SUCCESS_RESULT: ApiBaseResult = {
  success: true,
  code: 200,
  message: 'ok',
  data: {}
}

export class HttpProxy<Result = {}> {
  public static mockError = false
  public static token: string
  // 认证错误码
  public static readonly AUTH_ERROR_CODES = new Set([40040])

  public static commonErrorHandler(
    httpProxy: HttpProxy<any>,
    resData: ApiBaseResult | null,
    httpStatusCode: HTTPStatusCode,
    silent: boolean
  ) {
    console.log(`common error handler triggered: ${httpProxy.path}`, silent, {
      resData,
      httpStatusCode
    })
    if (!silent) {
      console.warn(
        'common error handler for non-silent api call:',
        httpProxy.path,
        'showing alert'
      )
    }
    if (resData) {
      if (this.AUTH_ERROR_CODES.has(resData.code)) {
        // TODO: auth error
      } else {
        if (!silent) {
          setTimeout(() => {
            return alert(`请求失败，${resData.message} ${resData.code}`)
          }, 1)
        }
      }
    }
  }

  private method: 'GET' | 'POST'
  private path: string
  private postRaw: boolean = false
  private defaultPrevented: boolean = false
  private headers: { [name: string]: string } = {}
  private urlArgs?: APIQueryArgCollection
  private bodyArgs?: APIBodyArgCollection
  private rawBody?: Blob | string
  private resultFilter: null | ((res: any) => Result) = null
  private mockResult?: Result & ApiBaseResult
  private mockDelay: number = 0
  private silent: boolean = false
  private axiosInstance: AxiosInstance

  // callbacks
  private onSuccess: null | ((result: Result & ApiBaseResult) => true | void) = null
  private onFail: null | ((httpStatusCode: HTTPStatusCode) => true | void) = null
  private onError: null | ((msg: string, statusCode: number, data?: any) => true | void) = null
  private onFailOrError: null | ((httpStatusCode: HTTPStatusCode, msg?: string,
                                  statusCode?: number,
                                  data?: any
  ) => true | void) = null
  private onAlways: null | (() => void) = null

  constructor(
    method: 'GET' | 'POST',
    path: string,
    urlArgs?: APIQueryArgCollection,
    body?: APIBodyArgCollection | Blob | string
  ) {
    this.axiosInstance = axios.create()
    this.method = method
    this.path = path
    this.urlArgs = urlArgs
    this.interceptorsResponse()
    if (method === 'POST') {
      if (body instanceof Blob || typeof body === 'string') {
        this.rawBody = body
        this.postRaw = true
      } else {
        this.bodyArgs = body
        this.postRaw = false
      }
    } else {
      if (body) {
        console.error('body:', body)
        throw new Error(`GET api call ${path} shouldn't have a body`)
      }
    }
    setTimeout(() => {
      this.send()
    }, 0)
  }
  /**
   * 设置api成功回调
   * @param callback 成功回调
   */
  public success(callback: (result: Result & ApiBaseResult) => true | void): this {
    this.onSuccess = callback
    return this
  }

  /**
   * 设置 api 失败回调，会在 http 请求不成功时触发
   * @param callback 失败回调，若返回 true 则会阻止默认的 alert 行为
   */
  public fail(callback: (httpStatusCode: HTTPStatusCode) => true | void): this {
    this.onFail = callback
    return this
  }

  /**
   * 设置 api 错误回调，会在 http 请求成功，但是响应结果中 success 字段为 false 时触发
   * @param callback 错误回调，若返回true则会阻止默认的alert行为
   */
  public error(
    callback: (msg: string, statusCode: number, data?: any) => true | void
  ): this {
    this.onError = callback
    return this
  }

  /**
   * 设置 api 失败或错误回调，会在 http 请求失败或者响应结果中 success 字段为 false 时触发
   * @param callback 错误或失败回调，若返回true则会阻止默认的alert行为
   */
  public failOrError(
    callback: (
      httpStatusCode: HTTPStatusCode,
      msg?: string,
      statusCode?: number,
      data?: any
    ) => true | void
  ): this {
    this.onFailOrError = callback
    return this
  }

  /**
   * 设置 api 结束回调，不论 api 成功或失败，都会调用此回调。适用场景是一些不需关心 api 本身是否失败的场景（比如按钮解锁）
   * @param callback 回调函数。函数没有参数，如果需要参数，应当使用其他的方法
   */
  public always(callback: () => void): this {
    this.onAlways = callback
    return this
  }

  /**
   * 阻止api结果处理环节中的默认操作（比如弹出默认的alert提示）
   */
  public preventDefault(): this {
    this.defaultPrevented = true
    return this
  }

  /**
   * 设置请求 header
   * @param name 名称
   * @param value 值
   */
  public setHeader(name: string, value: string): this {
    this.headers[name] = value
    return this
  }

  /**
   * 为 api 调用设置虚拟返回值，用于方便调试
   * @param data 虚拟返回值 必须和标准的返回值是同一格式
   */
  public mock(data: Result, mockDelay: number = 300): this {
    this.mockResult = Object.assign(data, BASE_SUCCESS_RESULT)
    this.mockDelay = mockDelay
    return this
  }

  /** 为 api 设置结果过滤器，只会在 api 调用成功时生效 */
  public addResultFilter<ServerResData>(filter: (res: ServerResData) => Result) {
    this.resultFilter = filter
    return this
  }

  /**
   * 禁用任何形式的 api 报错提示，但是保留通用错误处理（比如针对认证错误的处理）
   * 大部分情况下推荐使用此方法而不是 .preventDefault()
   */
  public markSilent() {
    this.silent = true
    return this
  }

  private send() {
    if (this.mockResult) {
      setTimeout(() => {
        console.warn('[mock] mock api result for api call:', this.path, this)
        if (this.onSuccess && this.mockResult) {
          this.onSuccess(this.mockResult)
        }
        if (this.onAlways) {
          this.onAlways()
        }
      }, this.mockDelay) // sim network latency for a little bit
      return
    }
    if (this.method === 'GET') {
      this.sendGet()
    } else {
      this.sendPost()
    }
  }

  private getAuthHeaders(): { [key: string]: string } {
    const { token } = storage.get(['token'])
    return { authorization: `${HttpProxy.token || token}` }
  }

  private async sendGet() {
    const headers = this.getAuthHeaders()
    for (const key in this.headers) {
      headers[key] = this.headers[key]
    }
    try {
      const res = await this.axiosInstance.request({
        baseURL: configs.apiHost,
        url: this.path,
        method: this.method,
        headers,
        params: utils.underlize(this.urlArgs as APIQueryArgCollection)
      })
      this.handleRes(true, res.status as HTTPStatusCode, res.data)
    } catch (err) {
      this.handleRes(false, err)
    }
  }

  private async sendPost() {
    const headers = this.getAuthHeaders()
    for (const key in this.headers) {
      headers[key] = this.headers[key]
    }
    if (this.postRaw) {
      let url = ''
      if (this.urlArgs) {
        url = utils.genApiUrl(`${configs.apiHost}/${this.path}`, utils.underlize(this.urlArgs))
      }
      this.postRawBody(url, headers)
    } else {
      headers['Content-Type'] = 'application/json'
      try {
        const res = await this.axiosInstance.request({
          method: this.method,
          baseURL: configs.apiHost,
          url: this.path,
          headers,
          data: this.bodyArgs,
          params: this.urlArgs
        })
        this.handleRes(true, res.status as HTTPStatusCode, res.data)
      } catch (err) {
        this.handleRes(false, err)
      }
    }
  }

  private postRawBody(url: string, headers: HttpProxy<Result>['headers']) {
    const xhr = new XMLHttpRequest()
    xhr.onload = () => {
      const data = xhr.response
      this.handleRes(true, xhr.status as HTTPStatusCode, data)
    }
    xhr.onerror = () => {
      this.handleRes(false, xhr.status as HTTPStatusCode)
    }
    xhr.open('POST', url, true)
    for (const key in headers) {
      const value = headers[key]
      xhr.setRequestHeader(key, value)
    }
    xhr.send(this.rawBody)
  }

  private handleRes(
    reqSuccess: boolean,
    httpStatusCode: HTTPStatusCode,
    data?: object | string
  ) {
    const preventCamelize = false
    let parsedData: Result & ApiBaseResult
    if (HttpProxy.mockError) {
      reqSuccess = false
      httpStatusCode = 509 as any
    }
    if (!reqSuccess) {
      // reqSuccess 为 false 说明 http 请求本身失败
      console.error(
        'HTTP Error ' + httpStatusCode + ' for api call: ' + this.path
      )
      let skipDefault = false
      if (this.onFail) {
        const ans = this.onFail(httpStatusCode)
        if (ans === true) {
          skipDefault = true
        }
      }
      if (this.onFailOrError) {
        const ans = this.onFailOrError(httpStatusCode)
        if (ans === true) {
          skipDefault = true
        }
      }
      if (!skipDefault && !this.defaultPrevented) {
        HttpProxy.commonErrorHandler(this, null, httpStatusCode, this.silent)
      }
    } else {
      // reqSuccess 为 true 说明 http 请求本身成功，需要进一步判断返回值
      let objData: object
      if (typeof data === 'string') {
        try {
          objData = JSON.parse(data)
        } catch (error) {
          console.error('parse api res error')
          console.log({ apiResData: data })
          console.error(error)
          objData = {
            success: false,
            code: 0,
            statusMessage: '无法解析请求返回值'
          }
        }
      } else {
        objData = data as object
      }
      if (!preventCamelize) {
        parsedData = utils.camelize(objData) as any
      } else {
        parsedData = objData as any
        parsedData.code = (data as any).code
        parsedData.message = (data as any).message
      }
      if (parsedData.success) {
        if (this.onSuccess) {
          if (this.resultFilter) {
            try {
              parsedData = Object.assign(
                BASE_SUCCESS_RESULT,
                this.resultFilter(parsedData)
              )
            } catch (error) {
              console.error(
                'Api resultFilter throwed an Error, api path:' + this.path
              )
              throw error
            }
          }
          try {
            this.onSuccess(parsedData)
          } catch (error) {
            console.error('Api Success Callback Error for ' + this.path)
            throw error
          }
        }
      } else {
        console.error('got error for api call', this.path)
        console.error(parsedData.code, parsedData.message, {
          httpProxy: this
        })
        let skipDefault = false
        if (this.onError) {
          const ans = this.onError(
            parsedData.message,
            parsedData.code,
            parsedData
          )
          if (ans === true) {
            skipDefault = true
          }
        }
        if (this.onFailOrError) {
          const ans = this.onFailOrError(
            httpStatusCode,
            parsedData.message,
            parsedData.code,
            parsedData
          )
          if (ans === true) {
            skipDefault = true
          }
        }
        if (!skipDefault && !this.defaultPrevented) {
          HttpProxy.commonErrorHandler(this, parsedData, 200, this.silent)
        }
      }
    }
    if (this.onAlways) {
      return this.onAlways()
    }
  }

  private interceptorsResponse() {
    // NOTE: 实现拦截器拦截 response
    this.axiosInstance.interceptors.response.use((response: AxiosResponse) => {
      return Promise.resolve(response)
    }, (error: Error) => {
      const list = error.message.split(' ')
      const code = list.pop()
      if (code === '403') {
        const { search, pathname } = location
        if (pathname === '/login') {
          return Promise.reject(error)
        }
        history.replaceState('', '', `/login?target_url=${pathname}${search}`)
        location.reload()
      }
      return Promise.reject(error)
    })

  }
}

export default HttpProxy
